feat: mirror opencode skills into .cursor/skills/ for Cursor agent discovery - #90
Merged
Conversation
…scovery Discover opencode's resolved skills (project + global + config.skills.paths), filter through permission config, and materialise them as a git-ignored mirror in <cwd>/.cursor/skills/ with a 'generated: opencode-cursor' sentinel. An <available_skills> catalogue is appended to the generated system rule so the Cursor agent can discover and load skills on demand. - New: src/plugin/skill-discovery.ts — filesystem walk, frontmatter parsing, permission filtering (map-form + rule-array), extraPaths, path expansion, skillSetHash including all files (mtime + size). - New: src/provider/skill-mirror.ts — materialisation with sentinel, git-ignore, idempotent writes, stale-dir pruning, user-owned protection, per-file 1MB skip, total 10MB cap, removeSkillMirror for dispose, buildSkillsCatalogue. - Updated: src/provider/system-rule.ts — writeSystemRule and resolveSystemDelivery accept optional skillsCatalogue appended to rule body. - Updated: src/plugin/index.ts — forwardSkills option (default true), skills.include/exclude override, materialisation in config hook, live re-sync in chat.params hook (hash-gated), currentSkillsCatalogue always forwarded per turn, removeSkillMirror in dispose. - Updated: src/provider/language-model.ts — dynamic + static catalogue resolution (no self-provisioning — respects forwardSkills:false). - Updated: src/provider/delegate.ts + src/plugin/cursor-tools.ts — settingSources: ['project'] passed to delegate's acquireAgent. - Updated: src/provider/index.ts — skillsCatalogue in provider options. - Tests: 403 passing (25 skill-discovery, 14 skill-mirror, 10 plugin-skill- mirror, 6 catalogue in language-model-system, plus existing). - Docs: README.md (Skills section), CHANGELOG.md ([Unreleased]), SECURITY.md (skills mirror threat model). - .gitignore: exclude .cursor/ (plugin-generated runtime artifacts).
Dirent.isDirectory() is false for a symlink pointing at a directory, so a skill linked in from a shared checkout was dropped from the mirror with no warning. Symlinked supporting files were lost the same way. Entries are now classified through the link target, with a resolved-path visited set guarding the cycles that following links admits.
skills.include is checked before permissions resolve, so it re-admits a denied skill by design — SECURITY.md read as though deny were absolute. Skills bundled in opencode plugin packages live in the package cache, which is not a scanned location, and the SDK exposes no skills API to resolve them. Documented so the omission is visible rather than silent.
Two conflicts, both resolved to keep each side's intent: - language-model.ts: #85/#86 moved the resolveSystemDelivery call inside the withSessionLock callback. The skills-catalogue lookup was re-placed into that relocated call rather than restored to its old position. - CHANGELOG.md: the skills-bridge entry stays under [Unreleased]; main's 0.6.2 and 0.7.0 release sections are kept in full.
0.7.0 (#85) moved the plugin's own diagnostics off console.* and onto opencode's structured logging API. The skills bridge predated that and still wrote to the terminal, leaving the only console.* calls in src/. Withheld skills and the mirror-failure path now pass structured extra fields instead of pre-formatted strings, so the reason codes are queryable rather than embedded in a message. Also corrects the plugin-cache path in the README limitation: it is platform-dependent, as PLUGIN_CACHE_PATH shows, and was documented as POSIX-only.
justin-carper
added a commit
that referenced
this pull request
Aug 3, 2026
The 0.7.1-next.0 section shipped three problems. - The skills bridge (#90) named no contributor. Wayne Simpson authored the original implementation commit; the release notes credited only the PR opener, and the Co-authored-by trailer in the squashed commit was the sole record. - The #89 entry said src/model-limits.ts is "regenerated on schedule". The weekly model-data-drift job runs --check and fails on staleness; it never writes. Regenerating is manual. It also named a "models generator CLI" that does not exist -- the script is npm run sync:model-limits -- and carried CI-internal detail (import-purity, stdout capture) that is invisible to users. - #88's dev-dependency bumps had no entry, breaking the convention every prior release follows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #78. Wayne's original commit (
cee7d16) is carried over unmodified,with authorship preserved; the three commits on top resolve the review feedback
left on that PR.
Summary
Mirrors opencode's resolved skills into
<cwd>/.cursor/skills/so the Cursoragent (and Cursor sub-agents) discover and load them natively via the
projectsettings layer. Skills are discovered from the standard opencode locations plus
config.skills.paths, filtered through the livepermissionconfig, andmaterialised as a git-ignored mirror stamped with a
generated: opencode-cursorsentinel. Only sentinel-bearing files are ever overwritten or deleted, so
user-owned
.cursor/skills/<id>/directories are safe. An<available_skills>catalogue is appended to the generated system rule, and the mirror re-syncs each
turn behind a hash gate. Full detail in #78.
Review fixes on top
de89036—fix(skills): follow symlinked skill dirs and filesDirent.isDirectory()returnsfalsefor a symlink pointing at a directory, soa skill linked in from a shared checkout was dropped from the mirror with no
warning. Symlinked supporting files were lost the same way in
collectFilesandcopyTree. Entries are now classified through the link target via a sharedentryKindhelper, with a resolved-path visited set guarding the cycles thatfollowing links admits. Broken symlinks and links to non-regular files are still
rejected. Six tests added, three of which were confirmed red before the fix.
053e4eb—docs(skills): correct deny scope, note plugin-skill gapskills.includeis checked before permissions resolve, so it re-admits a deniedskill by design. SECURITY.md read as though
denywere absolute — corrected,since that is the file someone reads when deciding whether
denyis a hardguarantee.
Skills bundled inside opencode plugin packages land in the package cache
(
~/.cache/opencode/packages/<pkg>/node_modules/<pkg>/skills/), which is not ascanned location, and
@opencode-ai/sdkexposes no skills API to resolve them.Now documented in the README limitations list with the
config.skills.pathsworkaround, so a user does not quietly receive a subset of their skills.
fb44fb6—style(plugin): normalize indentation in skills bridge blocksindex.tsblock re-tabbed to the surrounding level, de-dented closing bracescorrected, and tabs in the otherwise space-indented
cursor-tools.tsdelegatecall converted to spaces.
Verification
npm run typecheck && npm test && npm run buildLimitations
config.skills.urls(HTTP catalogs) are not mirrored.cursor_cloud_agenttargets a remote repo and does not inherit skills.cursor_delegatewith a non-sessioncwddoes not mirror into that cwd, butpasses
settingSources: ["project"]so a pre-existing mirror there loads.